/*!
 * 通用工具类
 * @namespace: u/util
 * @author: xovel
 * @note: 依赖 jQuery，需要提前引入 jQuery；部分方法依赖指定插件，使用时注意提前引入。
 */
(function (window, $, undefined) {
  'use strict';

  var util = {};

  /**
   * 回车事件
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Function} callback 对应的回车事件函数
   * @return 无
   */
  util.onEnter = function (selector, callback) {
    $(selector).on('keydown', function (e) {
      if (e.keyCode === 13) {
        return callback.call(this, e);
      }
    });
  };

  /**
   * layer 弹出层再封装
   * @param {Object} options layer 自带参数设置项
   * @return {Number} layer 弹出层标识
   * 依赖 layer 插件
   */
  util.layerOpen = function (options) {

    options = $.extend({
      type: 1,
      fix: true,
      maxmin: 0,
      maxWidth: 0,
      skin: 'demo-class',
      // move: false,
      shadeClose: false
    }, options);

    return layer.open(options);
  };

  /**
   * 带确定和取消按钮的 layer 弹层再封装
   * @param {Object} options layer 自带参数设置项
   * @return {Number} layer 弹出层标识
   * 依赖 layer 插件
   */
  util.layerOpen2 = function (options, title) {
    var cancelTitle = '您正在进行操作，确定要关闭？';
    if (title) {
      cancelTitle = typeof title === 'string' ? title : cancelTitle;
      options.cancel = function (index) {
        layer.confirm(cancelTitle, {
          yes: function (idx) {
            layer.close(index);
            layer.close(idx);
          }
        });
        return false;
      };
    }

    options = $.extend({
      type: 1,
      fix: true,
      maxmin: 0,
      maxWidth: 0,
      skin: 'demo-class',
      shadeClose: false,
      // move: false,
      btn: ['确定', '取消']
    }, options);

    return layer.open(options);
  };

  /**
   * 全屏幕弹层
   * @param {*} options
   */
  util.layerOpen3 = function (options) {
    options = $.extend({
      type: 1,
      fix: true,
      maxmin: 0,
      maxWidth: 0,
      skin: 'demo-class',
      shadeClose: false,
      move: false,
      area: [$(window).width() + 'px', $(window).height() + 'px']
    }, options);

    return layer.open(options);
  };

  /**
   * 清空表单
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @return jQuery 对象
   * disabled 元素会被 :hidden 伪类选中，这里也直接进行清空
   */
  util.emptyForm = function (selector) {
    // 自动添加#作为ID标识符，下同
    if (typeof selector === 'string' && !/^[#.[]/.test(selector)) {
      selector = '#' + selector;
    }
    var $form = $(selector);
    var filter = ':button, :submit, :reset';
    $form
      .find(':input')
      .not(filter)
      .val('')
      .removeAttr('checked')
      .removeAttr('selected');
    return $form;
  };

  /**
   * 重置表单
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Boolean} emptyHidden 清除隐藏的元素的值
   * @return jQuery 对象
   */
  util.resetForm = function (selector, emptyHidden) {
    if (typeof selector === 'string' && !/^[#.[]/.test(selector)) {
      selector = '#' + selector;
    }
    var $form = $(selector);
    $form.find('[name="id"]').val('');
    $form.find('label.error').remove();
    $form.find('.error').removeClass('error');

    if (emptyHidden) {
      $form.find('[name]:hidden').not(':checkbox').not(':radio').val('');
    }

    var _form = $form.filter('form')[0];
    // 确保执行重置方法的对象为一个form
    if (_form) {
      _form.reset();
    }

    return $form;
  };

  /**
   * 获取表单数据
   * @param {String|DOM|jQuery} selector jQuery选择器
   * @param {Boolean} doNotTrim 是否去除首尾空格，为 true 则不去除
   * @return {Object} 对应的数据对象
   */
  util.formData = function (selector, doNotTrim, doNotEscapeUnsafeChar) {
    if (typeof selector === 'string' && !/^[#.[]/.test(selector)) {
      selector = '#' + selector;
    }
    var $form = $(selector);
    var data = {};
    var checkList = {};

    $form.find('[name]').each(function () {
      var $this = $(this);
      var name = $this.attr('name');
      var type = $this.attr('type');
      var val = $this.val();
      var checked = $this.prop('checked') === true;

      if (!doNotTrim) {
        val = $.trim(val);
      }

      if (!doNotEscapeUnsafeChar) {
        val = val.replace(/[<>\/]/g, '*');
      }

      // 仅针对有名字并且未获取到值的情况进行处理
      if (!!name && data[name] === undefined) {
        if (type === 'radio') { // 处理单选框
          if (checked) {
            data[name] = val || '1'; // 单选框未出给出值时视为 1
          }
        } else if (type === 'checkbox') { // 处理复选框
          if (checked) {
            if (!checkList[name]) {
              checkList[name] = [];
            }
            checkList[name].push(val);
          }
        } else { // 其他情况直接取值
          data[name] = val;
        }
      }
    });

    // 针对复选框，有值的时候进行提取
    $.each(checkList, function (name, value) {
      if (value && value.length > 0) {
        data[name] = value;
      }
    });

    return data;
  };

  /**
   * 从对象中取键值，util 内置方法
   *   主要解决 `xxx.yyy` 形式的取值问题
   * @param {Object} data 被取值的对象
   * @param {String} key 键值
   * @return 键值对应的数据
   */
  util._readData = function (data, key) {
    var ret = data[key];
    var keys = key.split('.');
    var len = keys.length;
    if (ret === undefined && len > 0) {
      try {
        ret = data;
        while (ret && len--) {
          ret = ret[keys.shift()];
        }
      } catch (e) {

      }
    }
    return ret;
  };

  /**
   * 填充表单
   * @param {String|DOM|jQuery} selector jQuery选择器
   * @param {Objece} data 数据对象
   * @return jQuery对象
   */
  util.fillForm = function (selector, data) {
    if (typeof selector === 'string' && !/^[#.[]/.test(selector)) {
      selector = '#' + selector;
    }
    var $form = $(selector);

    $form.find('[name]').each(function () {
      var $this = $(this);
      var name = $this.attr('name');
      var type = $this.attr('type');
      var value = util._readData(data, name);
      if (type === 'radio') {
        if ($this.val() === value + '') {
          $this.prop('checked', true);
        }
      } else if (type === 'checkbox') {
        if (typeof value === 'string') {
          value = value.split(',');
        }
        if ($.isArray(value) && value.length > 0) {
          $.each(value, function (_, item) {
            if ($this.val() === item + '') {
              $this.prop('checked', true);
            }
          });
        }
      } else {
        // 解决 undefined 无法设置的问题
        if (value === null || value === undefined) {
          value = '';
        }
        $this.val(value);
      }
    });

    return $form;
  };

  /**
   * 填充表单Html值
   * 该方法针对形如 `data-value="{name}"` 形式的元素进行写 HTML 代码操作
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Object} data 数据对象
   * @return 无
   */
  util.formHtmlData = function (selector, data) {
    var $elem = $(selector);

    $elem.find('[data-value]').html('').each(function () {
      var $this = $(this);
      var key = $this.attr('data-value');
      var value = util._readData(data, key);
      $this.html(value);
    });

    return $elem;
  };

  /**
   * 根据data字段填充HTML
   * Deprecated
   * 该方法针对形如 `data-{name}` 形式的元素进行写 HTML 代码操作
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Object} data 数据对象
   * @return selector 对应的 jQuery 对象
   */
  util.htmlData = function (selector, data) {
    var $elem = $(selector);

    $.each(data, function (key, value) {
      $elem.find('[data-' + key + ']').html(value);
    });

    return $elem;
  };

  /**
   * 获取时间
   * @param {Number} date 解析成时间，当传入 undefined 或者 true 时，视为使用当前时间
   *   当传入日期对象时，直接使用之
   *   当传入纯数字时，视为日期的毫秒数并进行解析，如果需要传入诸如 `2017`，可在前后添加空格占位符
   *   new Date() 解析详情可参阅 `https://tc39.github.io/ecma262/#sec-date-constructor`
   * @param {String} format 时间格式，默认为 `yyyy-MM-dd`
   * @return 对应的时间格式字符串
   */
  util.getDate = function (date, format) {
    var _date;
    if (date === undefined || date === true) {
      _date = new Date();
    } else {
      if (!date) {
        return '';
      }
      // ! 优先级比 instanceof 高
      if (date instanceof Date) {
        _date = date;
      } else {
        _date = new Date(/^\d+$/.test(date) ? +date : date);
        if (isNaN(_date.getTime())) {
          // 尝试修复 2018-08-01T00:00:00
          if (typeof date === 'string') {
            _date = new Date(date.replace(' ', 'T'));
            if (isNaN(_date.getTime())) {
              return '';
            }
          } else {
            return ''; // 解析失败时 _date 为 `Invalid Date`，表现为所有原型链方法取值均为 NaN
          }
        }
      }
    }

    var ret = format || 'yyyy-MM-dd';

    var o = {
      'M+': _date.getMonth() + 1, // month
      'd+': _date.getDate(), // day
      'h+': _date.getHours(), // hour
      'm+': _date.getMinutes(), // minute
      's+': _date.getSeconds(), // second
      'S': _date.getMilliseconds() // millisecond
    };

    // 年份处理
    ret = ret.replace(/y+/i, function (year) {
      return ('' + _date.getFullYear()).substr(4 - year.length);
    });

    // 其他格式化处理
    for (var k in o) {
      ret = ret.replace(new RegExp(k), function (v) {
        // 补零操作
        if (v.length > 1) {
          return ('00' + o[k]).substr(('' + o[k]).length);
        }
        return o[k];
      });
    }

    return ret;

  };

  /**
   * 格式化时间
   * @param {datetime} date 传入的时间戳
   * @param {string} format 时间格式
   * @return {string} 格式化之后的时间字符串
   */
  util.dateFormat = function (date, format) {
    return util.getDate(date, format || 'yyyy-MM-dd hh:mm:ss');
  };

  // 拓展日期的原型链方法
  Date.prototype.format = function (format) {
    return util.dateFormat(this, format);
  };

  /**
   * 获取对应的时间戳
   * @param {datetime} date 传入的时间或者时间戳
   * @return {number} 时间戳
   */
  util.getTime = function (date) {
    var _date;
    if (date === undefined || date === true) {
      _date = new Date();
    } else {
      if (!date) {
        return 0;
      }
      // ! 优先级比 instanceof 高
      if (date instanceof Date) {
        _date = date;
      } else {
        _date = new Date(/^\d+$/.test(date) ? +date : date);
        if (isNaN(_date.getTime())) {
          return 0; // 解析失败时 _date 为 `Invalid Date`，表现为所有原型链方法取值均为 NaN
        }
      }
    }

    return _date.getTime();
  };

  /**
   * 返回一个时间戳
   * @param {string} date `yyyy-MM-dd` 格式的字符串
   * @param {any} type 传入的类型，默认为起始时间
   */
  util.getTimeStamp = function getTimeStamp(date, type) {
    var temp = date.split('-');
    var curDate = new Date();
    curDate.setFullYear(+temp[0]);
    curDate.setMonth(+temp[1] - 1);
    curDate.setDate(+temp[2]);
    curDate.setHours(type ? 23 : 0);
    curDate.setMinutes(type ? 59 : 0);
    curDate.setSeconds(type ? 59 : 0);
    curDate.setMilliseconds(type ? 999 : 0);
    return curDate.getTime();
  }

  /**
   * 返回一个指定的特定时间戳
   * @param {string|Date|number} str 传入的待解析时间对象
   * @param {boolean} isLast 是否为最后一秒
   * @return 时间戳
   */
  util.getTime2 = function getTime2(str, isLast) {
    var ret = util.getTime(str);
    var curDate = new Date(ret);
    if (isLast) {
      curDate.setHours(23);
      curDate.setMinutes(59);
      curDate.setSeconds(59);
      curDate.setMilliseconds(999);
    } else {
      curDate.setHours(0);
      curDate.setMinutes(0);
      curDate.setSeconds(0);
      curDate.setMilliseconds(0);
    }
    return curDate.getTime();
  };

  /**
   * 设置下拉框的 HTML 代码
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Array|Object} data 数据源
   * @param {String} value 下拉 option 的 value 取值字段
   * @param {String} name 下拉 option 的名字取值字段
   * @param {Boolean|String} isAll 是否在最开始显示 `全部`(该 option 的取值为空，文本可以直接进行指定，如 `请选择`)
   * @param {String} sv 默认选中的下拉值，该值根据 value 对应的值进行匹配
   * @return selector 对应的 jQuery 对象
   * @update 2017.6.20 改进：支持 data 参数传入对象 为对象时忽略 value 和 name 参数
   */
  util.selectHtml = function (selector, data, value, name, isAll, sv) {
    var $elem = $(selector);
    var html = '';

    if (isAll) {
      html = '<option value="' + (typeof isAll === 'number' ? isAll : '') + '">' + (isAll === true ? '全部' : isAll) + '</option>';
    }

    $.each(data, $.isArray(data) ? function (_, item) {
      var isSelected = item[value] === sv;
      html += '<option ' + (isSelected ? 'selected ' : '') + 'value="' + item[value] + '">' + item[name] + '</option>';
    } : function (key, v) {
      var isSelected = v === sv;
      html += '<option ' + (isSelected ? 'selected ' : '') + 'value="' + key + '">' + v + '</option>';
    });

    // 重设 HTML 时，清空默认的下拉值，防止 HTML 为空时 value 依旧显示的问题
    $elem.val('').html(html);
    return $elem;
  };

  /**
   * 提取链接中的字段值
   * @param {String} name 待搜索的字段名
   * @param {String} url 被检索的链接，默认为当前网页路径
   * @param {Boolean} raw 是否直接返回取得的结果（即不进行解码操作）
   * @return 对应的字段值，如果该值没有，则返回空串
   */
  util.getUrlParam = function (name, url, raw) {
    if (!url) {
      url = window.location.search;
    }

    if (!name || !url) {
      return '';
    }

    var param = '';
    var reg = new RegExp('(^|&|\\?)' + name + '=([^&#]*)(&|$|#)', 'i');
    var ret = url.match(reg);

    if (ret) {
      param = raw ? ret[2] : decodeURIComponent(ret[2]);
    }

    return param;
  };

  // 针对 layer 提示层的再封装
  // 提供以下方法：
  // success: 操作成功提示信息
  // warn: 警告信息
  // error: 错误信息
  util.tip = {
    success: function (str, time, callback) {
      return layer.msg(str, {icon: 1, time: time || 1000}, callback);
    },
    warn: function (str, time, callback) {
      return layer.msg(str, {icon: 0, time: time || 2000}, callback);
    },
    error: function (str, time, callback) {
      return layer.msg(str, {icon: 2, time: time || 2500}, callback);
    }
  };

  /**
   * 从 jqGrid 表格中获取选中的列的 ID
   * 该方法区别于 jqGrid 默认的方法，会按照 DOM 顺序获取
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Boolean} useNative 是否采用 jqGrid 自带的选取 selarrrow 方法
   * @param {Boolean} orderDesc 是否倒序返回
   * @return 选中的 ID 组成的数组，没有选中时返回空数组
   */
  util.getGridSelectedIds = function (selector, useNative, orderDesc) {
    var $grid = $(selector);
    var id;
    var ids = useNative ? $grid.jqGrid('getGridParam', 'selarrrow') : $grid.jqGrid('getDataIDs');
    var ret = [];
    for (var i = 0; i < ids.length; i++) {
      id = ids[i];
      if ($grid.find('#' + id).find('input.checkbox').is(':checked')) {
        ret.push(id);
      }
    }
    return orderDesc ? ret.reverse() : ret;
  };

  /**
   * 创建 jqGrid 表格
   * 需要提前引入 jqGrid 插件 `https://github.com/tonytomov/jqGrid`
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {Object} options 传入的 jqGrid 参数
   * @param {Boolean} fixOverflow 是否隐藏横向滚动条
   * @param {Boolean} resize 是否响应全局缩放事件（预留参数，未实现）
   * @param {Function} callback 操作完成后的回调函数，以当前 jqGrid 表格对象作为参数
   * @return 表格的 jQuery 对象
   *
   * 这里的表格是带分页的，不带分页的需要修改 jsonReader 参数
   */
  util.jqGrid = function (selector, options, fixOverflow, resize, callback) {
    var $grid = $(selector);
    options = $.extend(true, {
      datatype: 'local',
      mtype: 'GET',
      styleUI: 'Bootstrap',
      forceFit: !!fixOverflow, // 如果取消底部滚动条，该参数需同步设置为 true，避免表格伸缩出现后面数据不可查看的问题
      pagerpos: 'center',
      recordpos: 'right',
      rowNum: 20,
      rowList: ['10', '20', '30', '50'],
      viewrecords: true, // 是否开启显示记录数
      emptyrecords: '无数据显示',
      pgtext: '当前页：{0} 总页数：{1}',
      recordtext: '当前记录：{0} - {1} 总记录：{2}',
      prmNames: {
        rows: 'pageSize',
        page: 'pageNum'
      },
      jsonReader: {
        root: 'data',
        rowNum: 'pageSize',
        total: 'pages',
        page: 'pageNum',
        records: 'total',
        repeatitems: false
      },
      loadError: function (xhr) {
        if (xhr.status === 401) {
          // 权限不足
          // publicAjaxRequest.delToken();
        } else if (xhr.statusText === 'timeout') {
          util.tip.error('网络连接超时');
        } else if (xhr.status === 404) {
          util.tip.error('接口不存在或未准备好');
        } else if (xhr.status === 500) {
          util.tip.error('服务器错误');
        } else {
          util.tip.error('请求错误');
        }
      },
      loadBeforeSend: function (xhr) {
        // 设置请求头
        xhr.setRequestHeader('Content-Type', 'application/json');
        $grid.jqGrid('clearGridData');
        return util.checkToken(xhr);
      }
    }, options);

    // POST 类型时，包装请求数据
    if (options.mtype.toUpperCase() === 'POST' && !options.serializeGridData) {
      options.serializeGridData = function (postData) {
        return JSON.stringify(postData);
      };
    }

    // 默认排序全部去除
    $.each(options.colModel, function (_, mod) {
      mod.sortable = !!mod.sortable;
    });

    if (options.multiselect) {
      var _onSelectRow = options.onSelectRow;
      options.onSelectRow = function (id) { // 单选时，已禁用的项跳过
        var $check = $grid.find('tr#' + id).find('input.checkbox');
        if ($check.attr('disabled')) {
          $check.prop('checked', false);
          // $grid.jqGrid('setSelection', id, false);
        }
        if ($.isFunction(_onSelectRow)) {
          _onSelectRow.apply(null, arguments);
        }
      };
      var _onSelectAll = options.onSelectAll;
      options.onSelectAll = function () { // 全选时，已禁用的项跳过
        var ids = $grid.jqGrid('getDataIDs');
        var $check;
        ids.forEach(function (id, index) {
          $check = $grid.find('tr#' + id).find('input.checkbox');
          if ($check.attr('disabled')) {
            $check.prop('checked', false);
            // $grid.jqGrid('setSelection', id, false);
          }
        });
        if ($.isFunction(_onSelectAll)) {
          _onSelectAll.apply(null, arguments);
        }
      };

      if (options.multiboxonly) {
        // 重新规划 gridComplete 方法
        var _gridComplete = options.gridComplete;
        options.gridComplete = function () {
          // 单选时关闭头部的全选功能入口
          $grid.parents('.ui-jqgrid').find('.ui-jqgrid-htable').find('input.cbox').remove();
          // 类型修改为 radio
          $grid.parents('.ui-jqgrid').find('.ui-jqgrid-btable').find('input.cbox').attr('type', 'radio');
          if ($.isFunction(_gridComplete)) {
            _gridComplete();
          }
        };
        // 开启单选模式时，在选择时先进行重置
        options.beforeSelectRow = function () {
          $grid.jqGrid('resetSelection');
        };
      }
    }

    var _loadComplete = options.loadComplete;
    options.loadComplete = function (res) {

      if (res && res.source && res.code) {
        var code = +res.code;
        if (code !== 0 && code !== 200) {
          util.tip.error(res.msg || res.message || '请求错误');
        }
      }

      if ($.isFunction(_loadComplete)) {
        _loadComplete(res);
      }
    };

    $grid.jqGrid(options);

    if (fixOverflow) {
      $grid.parents('.ui-jqgrid-bdiv').css({'overflow-x': 'hidden'});
    }

    if ($.isFunction(callback)) {
      callback($grid);
    }

    return $grid;
  };

  // 通用搜索分页的 jqGrid 表格
  util.searchGrid = function (selector, data) {
    var $grid = $(selector);

    $grid.setGridParam({
      postData: data,
      page: 1,
      datatype: 'json'
    }).trigger('reloadGrid');

    return $grid;
  };

  /**
   * 统一回调入口方法
   * @param {Function} callback 状态码为 0 时的回调函数
   * @param {String} errMsg 状态码不为 0 时的弹出提示文字，为 false 时不做提示
   * @return {Function} 封装过后的回调函数
   */
  util.resCallback = function (callback, errMsg) {
    return function (res) {
      var code = +res.code;
      if (code === 0 || code === 200) {
        if ($.isFunction(callback)) {
          callback(res);
        }
      } else if (errMsg !== false) {
        util.tip.error(errMsg || res.msg || res.message);
      }
    };
  };

  /**
   * 校验输入 verify input
   * @param {String|DOM|jQuery} selector jQuery 选择器
   * @param {RegExp} pattern 校验的正则表达式
   * @param {Function} pattern 进行校验的函数，校验结果为该函数的返回值，接收当前值为参数，上下文为当前 jQuery 对象
   * @param {Function} callback 校验完毕时的回调，接收检验结果和当前值作为参数，上下文为当前 jQuery 对象
   * @param {String} callback 如果传入字符串，视为在校验失败后进行提示的文本
   * @param {String} kls 校验失败时的类名，默认为 `error`
   * @return {Boolean} 校验是否通过，true 表示通过
   */
  util.vInput = function (selector, pattern, callback, kls) {
    kls = kls || 'error';
    var $elem = $(selector);
    var cbType = typeof callback;

    if ($elem.length < 1) {
      return;
    }

    var val = $elem.val();

    var ret = $.isFunction(pattern) ? pattern.call($elem, val) : pattern.test(val);

    if (ret) {
      $elem.removeClass(kls);
    } else {
      $elem.addClass(kls);
      if (cbType === 'string') {
        util.tip.warn(callback);
      }
    }

    if (cbType === 'function') {
      callback.call($elem, ret, val);
    }

    return ret;
  };

  /**
   * 与当前时间相隔多少天
   * @param time 结束的时间
   * @return {Number} 天数
   * 当传入无法解析的时间时，返回 -1
   */
  util.dateDiff = function (time) {
    var now = $.now();

    var dateToCompare = util.getDate(time);

    if (!dateToCompare) {
      return -1; // 解析不成功直接返回 -1
    }

    var diff = now - new Date(dateToCompare + ' 00:00:00').getTime();
    var ret = diff / (1000 * 60 * 60 * 24);

    return Math.floor(ret); // 由零点开始，所以这里直接向下取整
  };

  /**
   * 获取字符串的长度
   * @param str {String} 传入的字符串
   * @param isByte {Boolean} 是否按字节数进行计算
   * @return {Integer} 长度
   */
  /* eslint-disable no-control-regex */
  util.strLen = function (str, isByte) {
    str = str + '';
    if (isByte) {
      // 非 ascii 字符视为两个字节长度
      return str.replace(/[^\x00-\xff]/g, '00').length;
    }
    return str.length;
  };

  /**
   * 检查并设置 token
   */
  util.checkToken = function (jqXHR) {
    var isValid = false;
    var curDate = util.getDate();

    if (localStorage.user) {
      var userData = JSON.parse(localStorage.user);

      var userTime = +userData.time;
      var now = Date.now();
      if (
        userData
        && userData.token
        && userData.time
        && curDate === util.getDate(userData.time)
        && now - userTime <= 300 * 60 * 1000
      ) {
        // 延长操作有效期
        userData.time = now;
        localStorage.user = JSON.stringify(userData);
        isValid = true;
        // 将用户数据挂载到全局变量上
        window.oUser = userData;
        // 给定了 xhr 对象，则设置其头信息
        if (jqXHR) {
          jqXHR.setRequestHeader('token', userData.token);
        }
      }

    }

    if (!isValid) {
      util.tip.warn('您的登录已失效，请重新登录', 2500, function () {
        // 清理所有缓存数据
        sessionStorage.clear();
        localStorage.clear();
        window.location.href = '/login.html';
      });
    }

    return isValid;
  };

  /**
   * 统一的 ajax 调用入口
   * @param {object} options
   */
  util.ajax = function (url, param, type, callback) {
    var options = {
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      url: url,
      type: type,
      data: param,
      timeout: 30000,
      beforeSend: function (jqXHR, settings) {
        window.layerLoadIndex = layer.load(1, { shade: [0.1, '#fff'] });
        return util.checkToken(jqXHR);
      },
      success: function (res) {
        if ($.isFunction(callback)) {
          callback(res);
        }
      },
      error: function (jqXHR, textStatus, errorThrown) {
        var res = jqXHR.responseJSON;
        if (jqXHR.status === 200) {
          callback(res);
        } else if (jqXHR.statusText === 'timeout') {
          layer.msg('请求超时', { icon: 0 });
        } else if (jqXHR.status === 404) {
          util.tip.error('接口不存在或未准备好');
        } else if (jqXHR.status === 500) {
          util.tip.error('服务器错误');
        } else if (res) {
          layer.msg(res.msg || res.message, { icon: 2 });
        } else {
          util.tip.error('请求错误');
        }
      },
      complete: function (jqXHR, textStatus) {
        layer.close(window.layerLoadIndex);
      }
    };

    return $.ajax(options);
  };

  /**
   * 针对 GET 请求的封装方法
   * @param {string} url 请求地址
   * @param {object} param 请求参数
   * @param {function} callback 回调函数
   */
  util.get = function get(url, param, callback, errMsg) {
    // 第二个参数直接为回调的，视为发送空数据
    if (typeof param === 'function') {
      errMsg = callback;
      callback = param;
      param = '';
    }
    return util.ajax(url, param, 'GET', util.resCallback(function (res) {
      if ($.isFunction(callback)) {
        callback(res);
      }
    }, errMsg));
  };

  /**
   * 针对 POST 请求的封装方法
   * @param {string} url 请求地址
   * @param {object} param 请求参数
   * @param {function} callback 回调函数
   */
  util.post = function post(url, param, callback, errMsg) {
    var data = null;
    // 第二个参数直接为回调的，视为发送空数据
    if (typeof param === 'function') {
      errMsg = callback;
      callback = param;
      param = '';
    }
    if (param) {
      data = JSON.stringify(param);
    }
    return util.ajax(url, data, 'POST', util.resCallback(function (res) {
      if ($.isFunction(callback)) {
        callback(res);
      }
    }, errMsg));
  };

  /**
   * 获取字典表数据
   * @param {string} type 后台字典
   * @param {string} name 本地缓存的名称，如果未给定，则忽略，要求以 dict 开头，如果不是，则强制添加。命名风格为全部小写，单词间隔使用下划线 `_`
   * @param {boolean} force 直接请求后台数据
   * @return {promise}
   */
  util.getDict = function getDict(type, name, force) {
    var dfd = $.Deferred();
    if (name && name.indexOf('dict') !== 0) {
      name = 'dict_' + name;
    }
    var data;
    if (name && !force && sessionStorage[name]) {
      data = JSON.parse(sessionStorage[name]);
      return dfd.resolve(data);
    }
    util.get(ajaxURL.SYSTEM + '/dict/getDictList?type=' + type, function (res) {
      data = res.data;
      if (name) {
        sessionStorage[name] = JSON.stringify(data);
      }
      dfd.resolve(data);
    });

    return dfd.promise();
  };

  /**
   * 同时请求多个字典数据
   * @param {array} list 字典请求列表，接受传入参数为一个字符数组
   * @return {promise}
   */
  util.getDicts = function getDicts(list, force) {
    var len = list.length;
    var ret = [];
    var resolvedNum = 0;
    var dfd = $.Deferred();

    $.each(list, function (_, item) {
      var type = item;
      var name = 'dict_' + item;
      var data;

      if (!force && sessionStorage[name]) {
        data = JSON.parse(sessionStorage[name]);
        ret[_] = data;
        resolvedNum++;
        if (resolvedNum === len) {
          dfd.resolve(ret);
        }
      } else {
        util.get(ajaxURL.SYSTEM + '/dict/getDictList?type=' + type, function (res) {
          data = res.data;
          sessionStorage[name] = JSON.stringify(data);
          ret[_] = data;
          resolvedNum++;
          if (resolvedNum === len) {
            dfd.resolve(ret);
          }
        });
      }
    });

    return dfd.promise();
  };

  /**
   * 所有请求完成之后执行回调
   * @param {array} reqs 请求的列表
   *    每个请求 req 的值如果为字符串，视为 GET 请求
   *    否则应该是一个对象，包含 url/type/data/callback 信息
   * @param {array} callbacks 回调函数列表，优先级高于 req.callback
   * @return {promise} 将所有请求的 response 数据组成的数组作为参数传回
   */
  util.promiseAll = function promiseAll(reqs, callbacks) {
    var len = reqs.length;
    var ret = [];
    var resolvedNum = 0;
    var dfd = $.Deferred();

    callbacks = callbacks || [];

    $.each(reqs, function (_, item) {
      var url, type = 'GET', data, callback = $.noop;
      if (typeof item === 'string') {
        url = item;
      } else {
        url = item.url;
        type = item.type || type;
        data = item.data;
        if ($.isFunction(item.callback)) {
          callback = item.callback;
        }
      }

      if (type.toUpperCase() === 'POST') {
        data = data ? JSON.stringify(data) : '';
      }

      if ($.isFunction(callbacks[_])) {
        callback = callbacks[_];
      }

      util.ajax(url, data, type, function (res) {
        var code = +res.code;
        if (code === 0 || code === 200) {
          callback(res);

          ret[_] = res;
          resolvedNum++;
          if (resolvedNum === len) {
            dfd.resolve(ret);
          }
        } else {
          dfd.reject(_);
        }
      }).fail(function () {
        dfd.reject(_);
      });

    });

    return dfd.promise();
  };


  /**
   * 检测表格中对应只能一个数据，然后进行方法的调用
   * @param {String|DOM|jQuery} seletor jQuery 选择器
   * @param {function} fn 执行的方法
   */
  util.oneCheck = function (seletor, fn) {
    var $grid = $(seletor);
    return function (e) {
      var ids = util.getGridSelectedIds($grid);
      if (ids.length === 0) {
        util.tip.warn('请选择一条数据');
        return false;
      }
      if (ids.length > 1) {
        util.tip.warn('最多选择一条数据');
        return false;
      }
      var id = ids[0];
      var rowData = $grid.jqGrid('getRowData', id);
      if ($.isFunction(fn)) {
        fn(id, rowData, e);
      }
    };
  };

  /**
   * 表单数据必填验证
   * @param {*} selector
   */
  util.checkForm = function (selector, exclude) {
    var $form = $(selector);
    var data = util.formData($form);

    var excludes = (exclude || 'xxx').split(' ');

    var specData = {};
    var $inputs = $form.find(':input').not('[readonly], :hidden');

    $inputs.each(function () {
      var name = $(this).attr('name');
      if (excludes.indexOf(name) === -1) {
        specData[$(this).attr('name')] = 1;
      }
    });

    for (var name in specData) {
      if (data[name] === '' || data[name] === undefined) {
        return false;
      }
    }

    return true;

  };

  /**
   * 返回一个包裹函数的方法
   * @param {function} fn 需要包裹的函数
   */
  util.makeFn = function makeFn(fn) {
    return function () {
      return fn;
    };
  };

  /**
   * 时间戳转换
   * @param {object} data 待处理的数据
   * @param {array} list 需要转成时间戳的属性列表
   */
  util.toTimeStamp = function (data, list) {
    list.forEach(function (name) {
      data[name] = util.getTime(data[name]);
    });
    return data;
  };

  /**
   * 返回图片对应的可访问路径
   * @param {string} id 图片编号
   */
  util.pic = function pic(id) {
    return (ajaxURL.IMAGE || '//image.hangkechina.com/') + id;
  };

  /**
   * 文件上传入口
   * @param {string|DOM|jQeury} selector jQuery 元素选择器
   * @param {RegExp} filters 过滤的文件类型的正则表达式，默认为图片类型
   * @param {function} callback 回调函数
   *
   * 需提前引入 jQuery 文件上传插件 jQuery-File-Upload，必备的两个文件为：
   *   - jQuery-File-Upload/js/vendor/jquery.ui.widget.js
   *   - jQuery-File-Upload/js/jquery.fileupload.js
   */
  util.upload = function upload(selector, filters, callback) {
    if (typeof filters === 'function') {
      callback = filters;
      filters = '';
    }

    // 验证图片扩展名的正则表达式
    var reImage = /(\.|\/)(gif|jpe?g|png)$/i;
    if (filters instanceof RegExp) {
      reImage = filters;
    }

    var $elem = $(selector);

    var curCallback = util.resCallback(function (res) {
      if ($.isFunction(callback)) {
        callback(res);
      }
    });

    // 临时创建一个文件域
    var $file = $('<input />').attr({
      type: 'file',
      name: 'file'
    });

    var loadIndex = '';

    $elem.on('click', function () {
      $file.val('');

      $file.fileupload({
        dataType: 'json',
        url: ajaxURL.SYSTEM + '/file/upload',
        acceptFileTypes: reImage,
        headers: {
          // http://blog.csdn.net/sanjay_f/article/details/47407063
          // https://stackoverflow.com/questions/17415084/multipart-data-post-using-python-requests-no-multipart-boundary-was-found
          // 'Content-Type': 'multipart/form-data',
          token: window.oUser && window.oUser.token
        },
        progressall: function (e, data) {
          //
        },
        done: function (e, data) {
          var result = data.result || {};

          layer.close(loadIndex);

          curCallback(result);

        },
        add: function (e, data) {
          var uploadErrors = [];
          if (!reImage.test(data.files[0].name)) {
            uploadErrors.push('请选择图片类型的文件！');
          }
          if (data.files[0].size > 5000000) {
            uploadErrors.push('文件过大！');
          }
          if (uploadErrors.length > 0) {
            layer.msg(uploadErrors[0], { icon: 2, time: 1000 });
          } else {
            loadIndex = layer.load(1, { shade: [0.1, '#fff'] });
            data.submit();
          }
        },
        error: function (res) {
          layer.close(loadIndex);
          util.tip.error(res.responseJSON && res.responseJSON.message || '服务器错误');
        }
      });

      $file.click();
    });
  };

  /**
   * 提供一个地图的弹出层，进行拖拽选址
   * 需提前引入高德地图和拖拽控件
   * @param {string|DOM} selector 选择器
   * @param {function} callback 回调函数
   * @param {object} options 传入的参数
   *   reset 属性开启之后将在弹层打开之后清空预览信息
   *   search 属性开启之后将在上开给一个输入框进行地址搜索用于快速定位
   *   preview 属性开启之后将在下方开启一个预览文本的显示，显示当前选择的地址和最近的路口
   *   dynamic 属性开启之后将拉取 selector 元素的 data-lng 和 data-lat 属性，不存在时则调用 init 方法，一般为设置地图中心操作，如城市中心定位，通过代码进行写入，init 方法传入 map 实例作为参数
   *   callback 为选择地址之后的回调方法，触发操作为下方的确认选择操作，传入参数为当前选择的位置信息和当前点击触发弹出层的元素
   */
  util.dragMap = function dragMap(options) {
    options = $.extend({
      // 高德选址组件的宽度
      width: 800,
      // 高德选址组件的高度
      height: 500,
      // 点击触发事件的命名空间
      namespace: 'pick',
      // 点开时是否重置之前的选择
      reset: true,
      // 提供快速搜索
      search: true,
      // 预览选择结果
      preview: true,
      // 地图定位数据是否动态获取
      dynamic: true,
      // 地图的默认缩放级别
      zoom: 16
    }, options || {});

    var selector = options.selector;

    if (!selector) {
      util.tip.warn('必须传入 selector 参数');
      return false;
    }

    var $div = $('<div />').appendTo('body').css({
      background: 'white',
      display: 'none',
      padding: '5px'
    });

    var uuid = options.namespace + $.now();
    var ename = 'click.' + options.namespace;

    if (options.search) {
      $('<input placeholder="请输入地址进行搜索" class="form-control form-width300">').attr('id', uuid + '_input').appendTo($div);
    }

    $('<div />').attr('id', uuid + '_map').css({
      width: options.width,
      height: options.height,
      margin: '5px 0'
    }).appendTo($div);

    var ret;

    var $preview = $('<div />');

    if (options.preview) {
      $preview.css({
        height: '30px',
        lineHeight: '30px',
        color: '#333',
        overflow: 'hidden'
      }).attr('id', uuid + '_preview').appendTo($div);
    }

    // 地图实例
    var map;

    AMapUI.loadUI(['misc/PositionPicker'], function (PositionPicker) {
      var mapOptions = {
        zoom: options.zoom
      };
      if (options.center) {
        mapOptions.center = options.center;
      }

      map = new AMap.Map(uuid + '_map', mapOptions);

      var positionPicker = new PositionPicker({
        mode: 'dragMap',
        map: map
      });

      positionPicker.on('success', function (positionResult) {
        ret = positionResult;
        if (options.preview) {
          $preview.html('当前选择地址：' + ret.address + '&emsp;最近的路口：' + ret.nearestJunction);
        }
      });
      positionPicker.on('fail', function (positionResult) {
        ret = '';
        $preview.html('');
      });

      positionPicker.start();
      map.panBy(0, 1);

      map.addControl(new AMap.ToolBar({
        liteStyle: true
      }));

      var auto = new AMap.Autocomplete({
        input: uuid + '_input'
      });
      AMap.event.addListener(auto, 'select', select);// 注册监听，当选中某条记录时会触发
      function select(e) {
        if (e.poi && e.poi.location) {
          map.setZoom(options.zoom);
          map.setCenter(e.poi.location);
        }
      }

    });

    // 挂载触发元素入口
    if (typeof selector === 'string') {
      selector = [selector];
    }

    $.each(selector, function (_, item) {
      var $elem = $(item);
      var callback = options.callback;
      var init = options.init;
      var prev = options.prev;
      var dynamic = options.dynamic;

      if ($.isArray(callback)) {
        callback = callback[_];
      }

      if ($.isArray(init)) {
        init = init[_];
      }

      if ($.isArray(prev)) {
        prev = prev[_];
      }

      if ($.isArray(dynamic)) {
        dynamic = dynamic[_];
      }

      $elem.off(ename).on(ename, function () {
        if ($.isFunction(prev) && prev() === false) {
          return false;
        }

        if (options.reset) {
          ret = '';
          $preview.html('');
          if (options.search) {
            $('#' + uuid + '_input').val('');
          }
        }

        if ($.isFunction(init)) {
          init(map);
        }

        if (options.dynamic) {
          var _lng = $elem.data('lng');
          var _lat = $elem.data('lat');
          if (_lng && _lat) {
            map.setZoomAndCenter(options.zoom, [_lng, _lat]);
          }
        }
        util.layerOpen({
          title: false,
          content: $div,
          btn: ['确认选择', '取消'],
          yes: function (index) {
            if (ret) {
              if ($.isFunction(callback)) {
                callback(ret, $elem);
              }
              if (options.dynamic) {
                $elem.data({
                  lng: ret.position.lng,
                  lat: ret.position.lat
                });
              }
              layer.close(index);
            } else {
              util.tip.warn('当前无选择结果，请重新选择一个地址');
            }
          }
        });
      });

    });


  };

  /**
   *
   * 后台返回价格处理
   * 分转换成元
   */
  util.price = function (num) {
    if (num) {
      num = num * 0.01;// 分到元
      num += '';// 转成字符串
      var reg = num.indexOf('.') > -1 ? /(\d{1,3})(?=(?:\d{3})+\.)/g : /(\d{1,3})(?=(?:\d{3})+$)/g;// 确定使用有哪个正则
      num = num.replace(reg, '$1,');// 千分位格式化
      num = parseFloat(num).toFixed(2);
      // return num + '元';
    } else {
      num = Number(num);
    }
    return num + ' 元';
  };

  /**
   * 分转成元，多条转换
   * @param {object} data 待处理的数据
   * @param {array} list 需要转成元属性列表
   */
  util.mulPrice = function (data, list) {
    list.forEach(function (name) {
      data[name] = util.price(data[name]);
    });
    return data;
  };

  /**
   * 前端数据作为传给后台
   * 显示为分，传给后台转换成元
   *
   */

  util.toYuan = function (num) {
    num = num * 100; // 元到分
    return num;
  };

  /**
   * 里程换算  米换成公里
   */
  util.toKilometer = function (mileage) {
    mileage = mileage * 0.001;
    return parseFloat(mileage).toFixed(2) + ' 公里';
  };

  /**
   * 里程换算，多条转换
   * @param {object} data 待处理的数据
   * @param {array} list 需要转成公里属性列表
   */
  util.mulToKilmeter = function (data, list) {
    list.forEach(function (name) {
      data[name] = util.toKilometer(data[name]);
    });
    return data;
  };

  // 数字转千分符
  util.toThousand = function (v, sep) {
    sep = sep || ',';
    v = v * 0.01;
    if (Number(v)) {
      return String(v).trim().replace(/^(\d+)((\.\d+)?)$/, function (_, a, b) {
        return a.replace(/(\d)(?=(?:\d{3})+$)/g, function (x) { return x + sep }) + b;
      });
    }
    // 数值换转失败则原值返回
    return v;
  };

  /**
   * 根据两点之间的经纬度计算直线距离
   * 传入的参数为两个时，接受数组或者对象
   *   为数组时，经度在前，纬度在后
   *   为对象时，经度的属性名为 lng，纬度的属性名为 lat
   * 传入的参数为四个时，分别为开始地经度、开始地纬度、目的地经度、目的地纬度
   * @return 计算之后的距离，单位为千米
   */
  util.getDistance = function getDistance() {
    var args = arguments;
    var lng1, lng2, lat1, lat2;

    // 辅助方法，转换为弧度
    var rad = function (v) {
      return v * Math.PI / 180.0;
    };

    if (args.length === 2) {
      if ($.isArray(args[0])) {
        lng1 = rad(args[0][0]);
        lat1 = rad(args[0][1]);
      } else {
        lng1 = rad(args[0].lng);
        lat1 = rad(args[0].lat);
      }
      if ($.isArray(args[1])) {
        lng2 = rad(args[1][0]);
        lat2 = rad(args[1][1]);
      } else {
        lng2 = rad(args[1].lng);
        lat2 = rad(args[1].lat);
      }
    } else if (args.length === 4) {
      lng1 = rad(args[0]);
      lat1 = rad(args[1]);
      lng2 = rad(args[2]);
      lat2 = rad(args[3]);
    } else {
      return -1;
    }

    // 地球半径，单位为千米
    var EARTH_RADIUS = 6378.137;

    var a = lat1 - lat2;
    var b = lng1 - lng2;

    var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));

    s = s * EARTH_RADIUS;

    s = Math.round(s * 10000) / 10000;

    return s;

  };

  // 基于 sha256 加密字符串操作方法上面的再一次特殊加密
  // 需要提前引入 sha256 方法
  util.sha666 = function (str) {
    var ret = sha256(str);
    return ret.substr(0, 16) + ret.substr(20, 63) + '666';
  };

  /**
   * 转为安全的 HTML 字符串
   * @param {string} str 待转化的字符串
   */
  util.safeHTML = function safeHTML(str) {
    return str
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/&/g, '&amp;')
      .replace(/\n/g, '<br>');
  };

  // noConflict
  var _u = window.u;
  var _util = window.util;
  util.noConflict = function (deep) {
    window.u = _u;
    if (deep) {
      window.util = _util;
    }
    return util;
  };

  // export
  /* eslint-disable no-multi-assign */
  window.u = window.util = util;
})(window, jQuery);
